package com.nervenets.general.service.impl;

import com.alibaba.fastjson.JSON;
import com.nervenets.general.annotation.DataUniqueness;
import com.nervenets.general.annotation.DataUniquenessField;
import com.nervenets.general.annotation.DataUseLogger;
import com.nervenets.general.entity.KeyValue;
import com.nervenets.general.enumeration.Action;
import com.nervenets.general.enumeration.SqlFun;
import com.nervenets.general.exception.LogicException;
import com.nervenets.general.hibernate.DomainObject;
import com.nervenets.general.jwt.util.JwtUtils;
import com.nervenets.general.model.SecurityUser;
import com.nervenets.general.model.SqlFunField;
import com.nervenets.general.service.BaseService;
import com.nervenets.general.utils.ClassUtils;
import com.nervenets.general.utils.HttpTools;
import com.nervenets.general.utils.StringUtils;
import com.nervenets.general.web.params.FormParams;
import com.nervenets.general.web.params.Params;
import com.nervenets.general.web.params.QueryParams;
import com.nervenets.general.web.params.UpdateParams;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.StaleObjectStateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.Tuple;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

import static com.nervenets.general.i18n.I18nTranslator.translate;

@Slf4j
@Transactional(rollbackFor = Exception.class)
public abstract class BaseServiceImpl<T extends DomainObject, Dao extends JpaRepository<T, Long> & JpaSpecificationExecutor<T>> implements BaseService<T> {
    @Autowired
    public Dao dao;
    @Autowired
    private EntityManager entityManager;

    protected Class<T> getServiceClass() {
        return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    /**
     * 处理配置统一的查询条件，比如：权限数据相关的条件
     */
    public void universalConditionsHandler(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        //默认不查询被删除的数据，若想查询出来，重构该方法
        predicates.add(builder.equal(root.get("deleted"), 0));
        //to to something
    }

    /**
     * action操作之前的检查 @DataUniqueness @DataUniquenessField 字段值重复配置
     *
     * @param action 操作类型
     * @param t      操作对象
     */
    protected void beforeCheck(Action action, T t, Params... params) {
        if (action.isAddOrUpdate()) {
            Class<T> serviceClass = getServiceClass();
            List<String> fields;
            String message = null;
            if (serviceClass.isAnnotationPresent(DataUniqueness.class)) {
                DataUniqueness uniqueness = serviceClass.getAnnotation(DataUniqueness.class);
                fields = Arrays.asList(uniqueness.fields());
                message = uniqueness.message();
            } else {
                fields = ClassUtils.getDeclaredAnnotationField(serviceClass, DataUniquenessField.class).stream().map(Field::getName).collect(Collectors.toList());
            }

            if (!fields.isEmpty()) {
                if (hasUniqueKey(t.getId(), new QueryParams<T>() {
                    @Override
                    public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                        for (String field : fields) {
                            addEqualsPredicate(root.get(field), ClassUtils.callMethod(t, ClassUtils.getterMethod(field)));
                        }
                    }
                })) {
                    if (StringUtils.isBlank(message)) {
                        String fieldNames = fields.stream().map(filedName -> {
                            ApiModelProperty property = ClassUtils.getFieldAnnotation(serviceClass, filedName, ApiModelProperty.class);
                            return (null == property ? filedName : property.value()) + ":" + ClassUtils.callMethod(t, ClassUtils.getterMethod(filedName));
                        }).collect(Collectors.joining(","));
                        throw new LogicException("【%s】已存在，请检查后重新提交", fieldNames);
                    } else {
                        throw new LogicException(message);
                    }
                }
            }
        }
    }

    /**
     * action操作之前的功能及拦截，默认什么都不做
     *
     * @param action 操作类型
     * @param t      操作对象
     */
    protected void before(Action action, T t, Params... params) {
        //nothing to do ...
    }

    /**
     * action操作之后的功能及拦截，默认什么都不做
     *
     * @param action 操作类型
     * @param t      操作对象
     */
    protected void after(Action action, T t, Params... params) {
        //nothing to do ...
    }

    /**
     * 异常处理
     *
     * @param action 操作类型
     * @param var1   操作对象
     * @param e      捕捉到的异常
     */
    protected void exceptionHandler(Action action, T var1, Exception e) {
        log.error("#{}{} {} error\n\n用户: {}\n\n内容: {}\n\n来源：{}\n\n异常：{}",
                getServiceClass().getSimpleName(),
                var1.getId(),
                action,
                JwtUtils.getUserNameOrNull(),
                JSON.toJSONString(var1),
                HttpTools.getHttpRequestUri(),
                e);
    }

    @Override
    @Transactional
    public <S extends BaseService<T>> T formSave(FormParams<T, S> params, HttpServletRequest request, HttpServletResponse response) {
        return this.save(params.submit((S) this, request, response), params);
    }

    @Override
    @Transactional
    public T save(T var1, Params... params) {
        if (null == var1) {
            throw new LogicException(translate("application.exception.data.null.save"));
        }
        Action action = null;
        try {
            boolean isNew = var1.isNew();
            SecurityUser user = JwtUtils.getUser();
            if (isNew && 0 == var1.getCreateBy()) {
                if (null != user) var1.setCreateBy(user.getId());
            }
            action = isNew ? Action.add : (var1.isDeleted() ? Action.delete : Action.update);
            beforeCheck(action, var1, params);
            before(action, var1, params);
            T result;
            try {
                result = dao.saveAndFlush(var1);
            } catch (ObjectOptimisticLockingFailureException | StaleObjectStateException | TransactionSystemException e) {
                throw new LogicException(translate("application.exception.data.changed.message") + JSON.toJSONString(var1));
            }
            after(action, var1, params);
            Class<T> serviceClass = getServiceClass();
            if (serviceClass.isAnnotationPresent(DataUseLogger.class)) {
                log.info("#{}{} saved\n\n用户: {}\n\n内容: {}\n\n来源：{}\n\n",
                        serviceClass.getSimpleName(), var1.getId(), JwtUtils.getUserNameOrNull(), JSON.toJSONString(var1), HttpTools.getHttpRequestUri());
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            this.exceptionHandler(action, var1, e);
            throw e;
        }
    }

    @Override
    @Transactional
    public List<T> saveAll(List<T> vars, Params... params) {
        List<T> results = new ArrayList<>();
        vars.forEach(t -> results.add(save(t, params)));
        return results;
    }

    @Override
    public boolean existsById(long var1) {
        return dao.existsById(var1);
    }

    @Override
    public Optional<T> findById(long var1) {
        Optional<T> optional = dao.findById(var1);
        Class<T> serviceClass = getServiceClass();
        if (serviceClass.isAnnotationPresent(DataUseLogger.class)) {
            log.info("#{}{} found:\n\n查询结果：{}\n\n操作来源：{}\n\n",
                    serviceClass.getSimpleName(), var1, JSON.toJSONString(optional.orElse(null)), HttpTools.getHttpRequestUri());
        }
        return optional;
    }

    @Override
    public T findOne(long var1, String... nullMessages) {
        T t = findById(var1).orElse(null);
        if (null == t) {
            String message = String.format(translate("application.exception.data.not.found"), var1);
            if (null != nullMessages && nullMessages.length > 0) {
                message = nullMessages[0];
            }
            throw new LogicException(HttpStatus.NOT_FOUND.value(), message);
        }
        return t;
    }

    @Override
    public T findOne(QueryParams<T> params) {
        final List<T> ts = findAll(params, 1);
        return ts.isEmpty() ? null : ts.get(0);
    }

    @Override
    public <Temp> Temp findOne(long var1, Class<Temp> tempClass, String... nullMessages) {
        final List<Temp> temps = findAll(new QueryParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                addEqualsPredicate(root.get("id"), var1);
            }
        }, tempClass);
        if (temps.isEmpty()) {
            String message = String.format(translate("application.exception.data.not.found"), var1);
            if (null != nullMessages && nullMessages.length > 0) {
                message = nullMessages[0];
            }
            throw new LogicException(HttpStatus.NOT_FOUND.value(), message);
        }
        return temps.get(0);
    }

    @Override
    public List<T> findAllByIds(List<Long> ids) {
        return dao.findAllById(ids);
    }

    @Override
    public <Temp> List<Temp> findAllByIds(List<Long> ids, Class<Temp> tempClass) {
        return findAll(new QueryParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                addInPredicate(root.get("id"), ids);
            }
        }, tempClass);
    }

    @Override
    @Transactional
    public int updateById(long id, String field, Object value) {
        return this.updateById(id, KeyValue.gen(field, value));
    }

    @Override
    @Transactional
    public int updateById(long id, KeyValue... keyValues) {
        return update(new UpdateParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                for (KeyValue v : keyValues) {
                    addFieldSet(v.getKey(), v.getValue());
                }
                addEqualsPredicate(root.get("id"), id);
            }
        });
    }

    @Override
    @Transactional
    public int update(UpdateParams<T> updateParams) {
        return this.update(updateParams, true);
    }

    private int update(UpdateParams<T> updateParams, boolean universalConditionAdd) {
        Class<T> entityClass = getServiceClass();
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        final CriteriaUpdate<T> update = builder.createCriteriaUpdate(entityClass);
        final CriteriaQuery<T> query = builder.createQuery(entityClass);
        Root<T> root = update.from(entityClass);

        List<Predicate> predicates = new ArrayList<>();
        updateParams.generateUpdateSpecification(predicates, root, update, query, builder);
        if (universalConditionAdd && updateParams.addUniversalCondition()) {
            universalConditionsHandler(predicates, root, query, builder);
        }
        update.where(predicates.toArray(new Predicate[predicates.size()]));

        return entityManager.createQuery(update).executeUpdate();
    }

    @Override
    @Transactional
    public void deleteById(long var1) {
        T t = findOne(var1);
        delete(t);
    }

    @Override
    public void deleteByIds(Iterable<Long> ids) {
        ids.forEach(this::deleteById);
    }

    @Override
    @Transactional
    public void delete(T var1) {
        try {
            beforeCheck(Action.delete, var1);
            before(Action.delete, var1);
            dao.delete(var1);
            after(Action.delete, var1);
        } catch (Exception e) {
            e.printStackTrace();
            this.exceptionHandler(Action.delete, var1, e);
            throw e;
        }
    }

    @Override
    @Transactional
    public void deleteAll(Iterable<? extends T> entities) {
        entities.forEach(this::delete);
    }

    @Override
    public int delete(QueryParams<T> params) {
        Class<T> entityClass = getServiceClass();
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        final CriteriaDelete<T> delete = builder.createCriteriaDelete(entityClass);
        final CriteriaQuery<T> query = builder.createQuery(entityClass);
        Root<T> root = delete.from(entityClass);

        List<Predicate> predicates = new ArrayList<>();
        params.generateCustomSpecification(predicates, root, query, builder);
        if (params.addUniversalCondition()) {
            universalConditionsHandler(predicates, root, query, builder);
        }
        delete.where(predicates.toArray(new Predicate[predicates.size()]));

        return entityManager.createQuery(delete).executeUpdate();
    }

    @Override
    public List<T> findAll() {
        return dao.findAll(defSpecification(null));
    }

    @Override
    public List<T> findAll(QueryParams<T> params) {
        return dao.findAll(defSpecification(params));
    }

    @Override
    public List<T> findAll(QueryParams<T> params, int size) {
        return findAll(params, PageRequest.of(0, size)).getContent();
    }

    @Override
    public List<T> findAll(QueryParams<T> params, int size, Sort var2) {
        return findAll(params, PageRequest.of(0, size, var2)).getContent();
    }

    @Override
    public List<T> findAll(QueryParams<T> params, Sort var2) {
        return dao.findAll(defSpecification(params), var2);
    }

    @Override
    public Page<T> findAll(QueryParams<T> params, Pageable var2) {
        return dao.findAll(defSpecification(params), var2);
    }

    @Override
    public List<T> findAll(Sort var2) {
        return this.findAll(null, var2);
    }

    @Override
    public Page<T> findAll(Pageable var2) {
        return this.findAll(null, var2);
    }

    @Override
    public List<HashMap<Object, Object>> findAll(String... fields) {
        return this.findAll(null, Pageable.unpaged(), fields).getContent();
    }

    @Override
    public Page<HashMap<Object, Object>> findAll(Pageable pageable, String... fields) {
        return this.findAll(null, pageable, fields);
    }

    @Override
    public List<HashMap<Object, Object>> findAll(QueryParams<T> params, String... fields) {
        return this.findAll(params, Pageable.unpaged(), fields).getContent();
    }

    @Override
    public Page<HashMap<Object, Object>> findAll(QueryParams<T> params, Pageable pageable, String... fields) {
        return PageableExecutionUtils.getPage(findAll(params, pageable, true, fields), pageable, () -> count(params));
    }

    protected List<HashMap<Object, Object>> findAll(QueryParams<T> params, Pageable pageable, boolean universalConditionAdd, String... fields) {
        if (null == fields || fields.length == 0) throw new LogicException("fields cannot be empty.");
        SqlFunField[] funFields = new SqlFunField[fields.length];
        for (int i = 0; i < fields.length; i++) {
            funFields[i] = new SqlFunField(SqlFun.none, fields[i], null);
        }
        return this.function(params, null, funFields, pageable, universalConditionAdd);
    }

    @Override
    public <Temp> List<Temp> findAll(Class<Temp> tempClass) {
        return this.findAll(null, tempClass);
    }

    @Override
    public <Temp> List<Temp> findAll(QueryParams<T> params, Class<Temp> tempClass) {
        return findAll(params, tempClass, true);
    }

    @Override
    public <Temp> List<Temp> findAll(QueryParams<T> params, Class<Temp> tempClass, int size) {
        return findAll(params, tempClass, PageRequest.of(0, size)).getContent();
    }

    protected <Temp> List<Temp> findAll(QueryParams<T> params, Class<Temp> tempClass, boolean universalConditionAdd) {
        return this.findAll(params, Pageable.unpaged(), tempClass, universalConditionAdd);
    }

    @Override
    public <Temp> Page<Temp> findAll(QueryParams<T> params, Class<Temp> tempClass, Pageable pageable) {
        return PageableExecutionUtils.getPage(findAll(params, pageable, tempClass, true), pageable, () -> count(params));
    }

    protected <Temp> List<Temp> findAll(QueryParams<T> params, Pageable pageable, Class<Temp> tempClass, boolean universalConditionAdd) {
        Field[] fields = ClassUtils.getAllDeclaredFields(tempClass);
        String[] fieldNames = new String[fields.length];
        for (int i = 0; i < fields.length; i++) {
            fieldNames[i] = fields[i].getName();
        }

        List<HashMap<Object, Object>> maps = this.findAll(params, pageable, universalConditionAdd, fieldNames);
        return JSON.parseArray(JSON.toJSONString(maps), tempClass);
    }

    @Override
    public long count() {
        return this.count(null);
    }

    @Override
    public long count(QueryParams<T> params) {
        return dao.count(defSpecification(params));
    }

    @Override
    public List<HashMap<Object, Object>> count(QueryParams<T> params, String... groupBys) {
        return this.function(params, groupBys, new SqlFunField[]{new SqlFunField(SqlFun.count, "id", Long.class)});
    }

    @Override
    public Long countDistinct(QueryParams<T> params, String field) {
        final SqlFunField countDistinctField = new SqlFunField(SqlFun.countDistinct, field, Long.class);
        List<HashMap<Object, Object>> maps = this.function(params, null, new SqlFunField[]{countDistinctField});
        if (maps.isEmpty()) return 0L;
        return (Long) maps.get(0).get(countDistinctField.alias());
    }

    @Override
    public List<HashMap<Object, Object>> countDistinct(QueryParams<T> params, String field, String... groupBys) {
        return this.function(params, groupBys, new SqlFunField[]{new SqlFunField(SqlFun.countDistinct, field, Long.class)});
    }

    @Override
    public BigDecimal sum(String field, QueryParams<T> params) {
        final SqlFunField sumField = new SqlFunField(SqlFun.sum, field, BigDecimal.class);
        List<HashMap<Object, Object>> maps = this.function(params, null, new SqlFunField[]{sumField});
        if (maps.isEmpty()) return BigDecimal.ZERO;
        return (BigDecimal) maps.get(0).get(sumField.alias());
    }

    @Override
    public List<HashMap<Object, Object>> sum(String... fields) {
        return this.sum(null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> sum(QueryParams<T> params, String... fields) {
        return this.sum(params, null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> sum(QueryParams<T> params, String[] groupBys, String... fields) {
        if (null == fields || fields.length == 0) throw new LogicException("fields cannot be empty.");
        SqlFunField[] funFields = new SqlFunField[fields.length];
        for (int i = 0; i < fields.length; i++) {
            funFields[i] = new SqlFunField(SqlFun.sum, fields[i], BigDecimal.class);
        }
        return this.function(params, groupBys, funFields);
    }

    @Override
    public BigDecimal avg(String field, QueryParams<T> params) {
        final SqlFunField avgField = new SqlFunField(SqlFun.avg, field, BigDecimal.class);
        List<HashMap<Object, Object>> maps = this.function(params, null, new SqlFunField[]{avgField});
        if (maps.isEmpty()) return null;
        return (BigDecimal) maps.get(0).get(avgField.alias());
    }

    @Override
    public List<HashMap<Object, Object>> avg(String... fields) {
        return this.avg(null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> avg(QueryParams<T> params, String... fields) {
        return this.avg(params, null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> avg(QueryParams<T> params, String[] groupBys, String... fields) {
        if (null == fields || fields.length == 0) throw new LogicException("fields cannot be empty.");
        SqlFunField[] funFields = new SqlFunField[fields.length];
        for (int i = 0; i < fields.length; i++) {
            funFields[i] = new SqlFunField(SqlFun.avg, fields[i], BigDecimal.class);
        }
        return this.function(params, groupBys, funFields);
    }

    @Override
    public BigDecimal max(String field, QueryParams<T> params) {
        final SqlFunField maxField = new SqlFunField(SqlFun.max, field, BigDecimal.class);
        List<HashMap<Object, Object>> maps = this.function(params, null, new SqlFunField[]{maxField});
        if (maps.isEmpty()) return null;
        return (BigDecimal) maps.get(0).get(maxField.alias());
    }

    @Override
    public List<HashMap<Object, Object>> max(String... fields) {
        return this.max(null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> max(QueryParams<T> params, String... fields) {
        return this.max(params, null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> max(QueryParams<T> params, String[] groupBys, String... fields) {
        if (null == fields || fields.length == 0) throw new LogicException("fields cannot be empty.");
        SqlFunField[] funFields = new SqlFunField[fields.length];
        for (int i = 0; i < fields.length; i++) {
            funFields[i] = new SqlFunField(SqlFun.max, fields[i], BigDecimal.class);
        }
        return this.function(params, groupBys, funFields);
    }

    @Override
    public BigDecimal min(String field, QueryParams<T> params) {
        final SqlFunField minField = new SqlFunField(SqlFun.min, field, BigDecimal.class);
        List<HashMap<Object, Object>> maps = this.function(params, null, new SqlFunField[]{minField});
        if (maps.isEmpty()) return null;
        return (BigDecimal) maps.get(0).get(minField.alias());
    }

    @Override
    public List<HashMap<Object, Object>> min(String... fields) {
        return this.min(null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> min(QueryParams<T> params, String... fields) {
        return this.min(params, null, fields);
    }

    @Override
    public List<HashMap<Object, Object>> min(QueryParams<T> params, String[] groupBys, String... fields) {
        if (null == fields || fields.length == 0) throw new LogicException("fields cannot be empty.");
        SqlFunField[] funFields = new SqlFunField[fields.length];
        for (int i = 0; i < fields.length; i++) {
            funFields[i] = new SqlFunField(SqlFun.min, fields[i], BigDecimal.class);
        }
        return this.function(params, groupBys, funFields);
    }

    @Override
    public List<HashMap<Object, Object>> function(QueryParams<T> params, String[] groupBys, SqlFunField[] fields) {
        return function(params, groupBys, fields, Pageable.unpaged(), true);
    }

    private List<HashMap<Object, Object>> function(QueryParams<T> params, String[] groupBys, SqlFunField[] fields, Pageable pageable, boolean universalConditionAdd) {
        if (null == fields || fields.length == 0) throw new LogicException("fields cannot be empty.");
        Class<T> entityClass = getServiceClass();
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tuple> query = builder.createTupleQuery();
        Root<T> root = query.from(entityClass);

        List<Selection<?>> selections = new ArrayList<>();
        if (groupBys != null && groupBys.length > 0) {
            for (String groupBy : groupBys) {
                selections.add(fieldPathBuilder(root, groupBy).alias(groupBy));
            }
        }
        for (SqlFunField field : fields) {
            switch (field.getFun()) {
                case sum:
                    selections.add(builder.coalesce(builder.sum(fieldNumberPathBuilder(root, field.getName())), 0).as(field.getCast()).alias(field.alias()));
                    break;
                case avg:
                    selections.add(builder.coalesce(builder.avg(fieldNumberPathBuilder(root, field.getName())), 0).as(field.getCast()).alias(field.alias()));
                    break;
                case max:
                    selections.add(builder.coalesce(builder.max(fieldNumberPathBuilder(root, field.getName())), 0).as(field.getCast()).alias(field.alias()));
                    break;
                case min:
                    selections.add(builder.coalesce(builder.min(fieldNumberPathBuilder(root, field.getName())), 0).as(field.getCast()).alias(field.alias()));
                    break;
                case count:
                    selections.add(builder.coalesce(builder.count(fieldNumberPathBuilder(root, field.getName())), 0).as(field.getCast()).alias(field.alias()));
                    break;
                case countDistinct:
                    selections.add(builder.coalesce(builder.countDistinct(fieldNumberPathBuilder(root, field.getName())), 0).as(field.getCast()).alias(field.alias()));
                    break;
                default:
                    selections.add(fieldPathBuilder(root, field.getName()).alias(field.alias()));
                    break;
            }
        }
        if (null != params) params.generateCustomSelections(selections, root, query, builder);
        query.multiselect(selections);

        List<Predicate> predicates = new ArrayList<>();
        if (null != params) {
            params.generateCustomSpecification(predicates, root, query, builder);
            if (universalConditionAdd && params.addUniversalCondition()) {
                universalConditionsHandler(predicates, root, query, builder);
            }
        } else {
            if (universalConditionAdd) {
                universalConditionsHandler(predicates, root, query, builder);
            }
        }
        query.where(predicates.toArray(new Predicate[predicates.size()]));
        if (groupBys != null && groupBys.length > 0) {
            query.groupBy(Arrays.stream(groupBys).map(groupBy -> fieldPathBuilder(root, groupBy)).collect(Collectors.toList()));
            query.orderBy(new ArrayList<>());
        }

        List<HashMap<Object, Object>> results = new ArrayList<>();
        TypedQuery<Tuple> typedQuery = entityManager.createQuery(query);
        if (null != pageable && pageable.isPaged()) {
            typedQuery.setFirstResult(pageable.getPageNumber() * pageable.getPageSize()).setMaxResults(pageable.getPageSize());
        }
        List<Tuple> tuples = typedQuery.getResultList();
        if (tuples.isEmpty()) return results;
        for (Tuple tuple : tuples) {
            int i = 0;
            HashMap<Object, Object> item = new HashMap<>();
            for (Selection<?> selection : selections) {
                item.put(selection.getAlias(), tuple.get(i++));
            }
            results.add(item);
        }
        return results;
    }

    private Expression<?> fieldPathBuilder(Root<T> root, String fieldString) {
        if (fieldString.contains(".")) {
            String[] fields = fieldString.split("\\.");
            Path<T> path = null;
            for (String field : fields) {
                path = null == path ? root.get(field) : path.get(field);
            }
            return path;
        }
        return root.get(fieldString);
    }

    private Expression<? extends Number> fieldNumberPathBuilder(Root<T> root, String fieldString) {
        if (fieldString.contains(".")) {
            String[] fields = fieldString.split("\\.");
            Path<? extends Number> path = null;
            for (String field : fields) {
                path = null == path ? root.get(field) : path.get(field);
            }
            return path;
        }
        return root.get(fieldString);
    }

    @Override
    public boolean hasUniqueKey(Long id, String field, Object value) {
        return hasUniqueKey(id, KeyValue.gen(field, value));
    }

    @Override
    public boolean hasUniqueKey(Long id, KeyValue... keyValues) {
        if (null == keyValues || keyValues.length == 0)
            throw new IllegalArgumentException("unique conditions can not be null or empty.");
        return hasUniqueKey(id, generateUniqueKeyValueQueryParams(keyValues));
    }

    @Override
    public boolean hasUniqueKey(Long id, QueryParams<T> params) {
        return count(generateUniqueQueryParams(id, params)) > 0;
    }

    @Override
    public T findUniqueValue(Long id, String field, Object value) {
        return findUniqueValue(id, KeyValue.gen(field, value));
    }

    @Override
    public T findUniqueValue(Long id, KeyValue... keyValues) {
        if (null == keyValues || keyValues.length == 0)
            throw new IllegalArgumentException("unique conditions can not be null or empty.");
        return findUniqueValue(id, generateUniqueKeyValueQueryParams(keyValues));
    }

    @Override
    public T findUniqueValue(Long id, QueryParams<T> params) {
        final List<T> list = findAll(generateUniqueQueryParams(id, params));
        return list.isEmpty() ? null : list.get(0);
    }

    private QueryParams<T> generateUniqueKeyValueQueryParams(KeyValue[] keyValues) {
        return new QueryParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                Arrays.stream(keyValues).map(v -> builder.equal(root.get(v.getKey()), v.getValue())).forEach(predicates::add);
            }
        };
    }

    private QueryParams<T> generateUniqueQueryParams(Long id, QueryParams<T> params) {
        return new QueryParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                predicates.add(builder.notEqual(root.get("id"), null == id ? 0L : id));
                if (null != params) params.generateCustomSpecification(predicates, root, query, builder);
            }
        };
    }

    protected Specification<T> defSpecification(QueryParams<T> params) {
        return (root, query, builder) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (null != params) {
                params.generateCustomSpecification(predicates, root, query, builder);
                if (params.addUniversalCondition()) {
                    universalConditionsHandler(predicates, root, query, builder);
                }
            } else {
                universalConditionsHandler(predicates, root, query, builder);
            }
            return builder.and(predicates.toArray(new Predicate[predicates.size()]));
        };
    }
}
